package lumis.portal.principal.importprincipal.reader;

import lumis.portal.PortalException;
import lumis.portal.principal.importprincipal.ImportPrincipalStringResource;
import lumis.util.TextUtil;
import lumis.util.XmlUtil;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.Attributes;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.Control;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.ldap.LdapContext;
import javax.naming.ldap.PagedResultsControl;
import javax.naming.ldap.PagedResultsResponseControl;

import org.w3c.dom.Node;

/**
 * Implementation of IPrincipalReader for Active Directory
 * 
 *
 * @version $Revision: 13092 $ $Date: 2011-05-28 18:19:06 -0300 (Sat, 28 May 2011) $
 * @since 4.0.8
 */
public class ActiveDirectoryPrincipalReader implements IPrincipalReader
{
	private static int PAGESIZE = 200;

	private String domain;
	private String netbiosDomain = null;
	private Collection<LdapContext> contexts = new ArrayList<LdapContext>();

	public Map<String, String> read(Node reader) throws PortalException
	{
		String strUser;
		String strGroup;

		HashMap<String, String> returnValue = new HashMap<String, String>();

		domain = XmlUtil.readNodeString("domain", reader);
		netbiosDomain = XmlUtil.readNodeString("netbiosDomain", reader);

		String user = XmlUtil.readNodeString("credential/user", reader);
		String password = XmlUtil.readNodeString("credential/password", reader);
		
		Node[] uriNodes = XmlUtil.selectNodes("uris/uri", reader);
		for (Node uriNode: uriNodes)
		{
			String uri = XmlUtil.getTextContent(uriNode);
			contexts.add(getContext(uri, user, password));
		}

		strUser = readUsers();
		strGroup = readGroups();

		returnValue.put("userList", strUser);
		returnValue.put("groupList", strGroup);

		return returnValue;

	}
	
	private String readUsers() throws PortalException
	{
		StringBuilder result = new StringBuilder(4000);
		for (LdapContext ldapContext: contexts)
			readUsers(result, ldapContext);
		return result.toString();
	}
	
	private String readGroups() throws PortalException
	{
		StringBuilder result = new StringBuilder(4000);
		for (LdapContext ldapContext: contexts)
			readGroups(result, ldapContext);
		return result.toString();
	}

	private void readUsers(StringBuilder sbUser, LdapContext context) throws PortalException
	{
		try
		{
			byte[] cookie = null;
			do
			{
				NamingEnumeration results = context.search("", "(objectclass=user)", new SearchControls());

				while (results != null && results.hasMore())
				{
					SearchResult entry = (SearchResult) results.next();
					Attributes attributes = entry.getAttributes();

					// read primary login for Lumis Portal
					String login = getUserPrimaryLogin(attributes);
					if (login == null)
						continue;
					
					sbUser.append("<user><login>" + XmlUtil.encodeXml(login) + "</login>");

					boolean hasWindows2000Login = login.indexOf('@') != -1;
					if (hasWindows2000Login && netbiosDomain != null)
					{
						// read netbiosLogin
						Attribute samAccountName = attributes.get("sAMAccountName");
						if (samAccountName != null)
						{
							String samAccountNameValue = samAccountName.get().toString();
							if (samAccountNameValue != null && samAccountNameValue.length() > 0)
							{
								// add netbiosLogin as alternative login
								sbUser.append("<alternativeLogins><alternativeLogin>" + 
										XmlUtil.encodeXml(netbiosDomain + "\\" + samAccountNameValue) + 
										"</alternativeLogin></alternativeLogins>");
							}
						}
					}

					Attribute mailAttribute = attributes.get("mail");
					String email = mailAttribute == null ? "" : mailAttribute.get().toString();
					sbUser.append("<email>" + XmlUtil.encodeXml(email) + "</email>");

					Attribute givenName = attributes.get("givenName");
					String firstName = givenName == null ? "" : givenName.get().toString();
					if (firstName.length() == 0)
					{
						// use displayName as firstName and do not enter lastName
						Attribute displayNameAttrib = attributes.get("displayName");
						String displayName = displayNameAttrib == null ? "" : displayNameAttrib.get().toString();
						sbUser.append("<firstName>" + XmlUtil.encodeXml(displayName) + "</firstName>");
					}
					else
					{
						// use firstName and lastName
						sbUser.append("<firstName>" + XmlUtil.encodeXml(firstName) + "</firstName>");

						Attribute surName = attributes.get("sn");
						String lastName = surName == null ? "" : surName.get().toString();
						sbUser.append("<lastName>" + XmlUtil.encodeXml(lastName) + "</lastName>");
					}
					
					sbUser.append("<channelId></channelId></user>");
				}

				Control[] controls = context.getResponseControls();
				if (controls != null)
				{
					for (int i = 0; i < controls.length; i++)
					{
						if (controls[i] instanceof PagedResultsResponseControl)
						{
							PagedResultsResponseControl prrc = (PagedResultsResponseControl) controls[i];
							cookie = prrc.getCookie();
						}
					}
				}
				// Re-activate paged results
				context.setRequestControls(new Control[] { new PagedResultsControl(PAGESIZE, cookie, Control.CRITICAL) });
			}
			while (cookie != null);
		}
		catch (NamingException e)
		{
			throw createPortalException(context, e);
		}
		catch (IOException IOe)
		{
			throw new PortalException("STR_INVALID_FILE", IOe);
		}
	}

	/**
	 * Returns the primary login for the user with the given attributes.
	 * @param userAttributes the user's attributes.
	 * @return the primary login or null if the user has no login available.
	 * @since 4.0.11
	 */
	private String getUserPrimaryLogin(Attributes userAttributes) throws NamingException
	{
		Attribute userPrincipalName = userAttributes.get("userPrincipalName");
		if (userPrincipalName != null)
		{
			// use windows 2000 login as primary login
			return userPrincipalName.get().toString();
		}
		else
		{
			// read netbiosLogin, if available
			if (netbiosDomain != null)
			{
				Attribute samAccountName = userAttributes.get("sAMAccountName");
				if (samAccountName != null)
				{
					String netbiosLogin = samAccountName.get().toString();
					if (netbiosLogin != null && netbiosLogin.length() > 0)
						return netbiosDomain + "\\" + netbiosLogin;
				}
			}
		}
		
		return null;
	}

	private void readGroups(StringBuilder sbGroup, LdapContext context) throws PortalException
	{
		try
		{
			byte[] cookie = null;
			do
			{
				NamingEnumeration results = context.search("", "(objectclass=group)", new SearchControls());

				while (results != null && results.hasMore())
				{
					SearchResult entry = (SearchResult) results.next();
					Attributes attributes = entry.getAttributes();

					sbGroup.append("<group>");

					Attribute cnAttribute = attributes.get("cn");
					String shortId = cnAttribute == null ? "" : cnAttribute.get().toString();
					sbGroup.append("<shortId>" + XmlUtil.encodeXml(shortId + "@" + domain) + "</shortId>");

					Attribute nameAttribute = attributes.get("name");
					String name = nameAttribute == null ? "" : nameAttribute.get().toString();
					sbGroup.append("<name>" + XmlUtil.encodeXml(name) + "</name>");

					Attribute descriptionAttribute = attributes.get("description");
					String email = descriptionAttribute == null ? "" : descriptionAttribute.get().toString();
					sbGroup.append("<description>" + XmlUtil.encodeXml(email) + "</description>");

					sbGroup.append("<channelId></channelId>");

					sbGroup.append("<members>");

					if (attributes.get("member") != null)
					{
						NamingEnumeration groups = attributes.get("member").getAll();
						while (groups.hasMore())
						{
							String g = (String) groups.next();
							String memberId = getGroupMemberIdentifier(g);
							if (memberId != null)
								sbGroup.append("<member>" + XmlUtil.encodeXml(memberId) + "</member>");
						}
					}
					sbGroup.append("</members></group>");
				}

				Control[] controls = context.getResponseControls();
				if (controls != null)
				{
					for (int i = 0; i < controls.length; i++)
					{
						if (controls[i] instanceof PagedResultsResponseControl)
						{
							PagedResultsResponseControl prrc = (PagedResultsResponseControl) controls[i];
							cookie = prrc.getCookie();
						}
					}
				}
				// Re-activate paged results
				context.setRequestControls(new Control[] { new PagedResultsControl(PAGESIZE, cookie, Control.CRITICAL) });

			} while (cookie != null);

		}
		catch (NamingException e)
		{
			throw createPortalException(context, e);
		}
		catch (IOException IOe)
		{
			throw new PortalException("STR_INVALID_FILE", IOe);
		}
	}

	private LdapContext getContext(String uri, String user, String password) throws PortalException
	{
		LdapContext returnValue = null;
		try
		{
			Hashtable<String, String> env = new Hashtable<String, String>();
			env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
			env.put(Context.SECURITY_AUTHENTICATION, "simple");
			env.put(Context.SECURITY_PRINCIPAL, user);
			env.put(Context.SECURITY_CREDENTIALS, password);
			env.put(Context.PROVIDER_URL, uri);

			returnValue = new InitialLdapContext(env, null);
			returnValue.setRequestControls(new Control[] { new PagedResultsControl(PAGESIZE, Control.CRITICAL) });
		}
		catch (NamingException n)
		{
			throw createPortalException(uri, n);
		}
		catch (IOException ioe)
		{
			throw new PortalException("STR_INVALID_FILE", ioe);
		}
		return returnValue;
	}

	/**
	 * Returns the identifier for a group member.
	 * @param member the member's distinguished name.
	 * @return the member's identifier, or null if it was not found.
	 * @throws NamingException
	 * @since 4.0.11
	 */
	private String getGroupMemberIdentifier(String member) throws NamingException
	{
		final String cleanedDN = cleanDN(member);
		for (LdapContext context: contexts)
		{
			NamingEnumeration results = context.search("", "(distinguishedName=" + cleanedDN + ")",new SearchControls());
			while (results != null && results.hasMore())
			{
				SearchResult entry = (SearchResult) results.next();
				Attributes attributes = entry.getAttributes();
				
				// verify if this entry is a user or a group
				boolean user = false;
				Attribute objectClassAttrib = attributes.get("objectClass");
				NamingEnumeration objectClasses = objectClassAttrib.getAll();
				while (objectClasses.hasMoreElements())
				{
					Object objectClass = objectClasses.next();
					if ("user".equals(objectClass))
					{
						user = true;
						break;
					}
					else if ("group".equals(objectClass))
					{
						break;
					}
				}

				if (user)
				{
					return getUserPrimaryLogin(attributes);
				}
				else
				{
					Attribute cnAttribute = attributes.get("cn");
					if (cnAttribute != null)
						return cnAttribute.get().toString() + "@" + domain;
				}
			}
		}
		return null;
	}
	
	/**
	 * Create a {@link PortalException} due to a {@link NamingException} while
	 * accessing the given LDAP context.
	 * @param context the context.
	 * @param namingException the NamingException.
	 * @return the created PortalException.
	 * @since 4.0.10
	 */
	private PortalException createPortalException(Context context, NamingException namingException)
	{
		// obtain uri to display it on error message
		String uri;
		try
		{
			uri = context.getEnvironment().get(Context.PROVIDER_URL).toString();
		}
		catch (NamingException e)
		{
			// could not obtain uri from enviornment, so don't display it on error message
			uri = "";
		}
		
		return createPortalException(uri, namingException);
	}
	
	/**
	 * Create a {@link PortalException} due to a {@link NamingException} while
	 * accessing the given uri.
	 * @param uri the uri.
	 * @param namingException the NamingException.
	 * @return the created PortalException.
	 * @since 4.0.10
	 */
	private PortalException createPortalException(String uri, NamingException namingException)
	{
		// the framework sometimes put a zero character in its message that
		// breaks up the portal. Replacing it by space.
		String exceptionMessage = namingException.toString().replace((char)0, ' ');
		
		return new PortalException("STR_LDAP_ACCESS_ERROR;" + 
				TextUtil.escapeLocalizationParameter(uri) + ";" + 
				TextUtil.escapeLocalizationParameter(exceptionMessage), 
				new ImportPrincipalStringResource(), namingException);
	}

	/**
	 * Clears the DN of LDAP search filter.
	 * 
	 * @param dn
	 *            the DN.
	 * @return the cleaned DN.
	 * @since 6.0.0
	 */
	private String cleanDN(String dn)
	{
		StringBuilder newDn = new StringBuilder();
		for (String rdn : dn.split(",(?=[a-zA-Z]+=)"))
		{
			newDn.append(cleanRDN(rdn));
			newDn.append(",");
		}

		return newDn.substring(0, newDn.length() - 1);
	}

	/**
	 * Clears the RDN's value.
	 * 
	 * @param rdn
	 *            the RDN.
	 * @return the cleaned RDN.
	 * @since 6.0.0
	 */
	private String cleanRDN(String rdn) {
		final int indexOfEq = rdn.indexOf("=");
		final String attribute = rdn.substring(0, indexOfEq);
		final String value = rdn.substring(indexOfEq + 1);
		return attribute + "=" + cleanReservedKeywords(value);
	}

	/**
	 * Clears reserved keywords of the LDAP search value.
	 * 
	 * @param value
	 *            the search value.
	 * @return the cleaned value.
	 * @since 6.0.0
	 */
	private String cleanReservedKeywords(String value)
	{
		return value
			.replaceAll("\\Q\\\\E", "\\\\5C")
			.replaceAll("\\Q\"\\E", "\\\\22")
			.replaceAll("\\Q+\\E", "\\\\2B")
			.replaceAll(",", "\\\\2C")
			.replaceAll("/", "\\\\2F")
			.replaceAll(";", "\\\\3B")
			.replaceAll("<", "\\\\3C")
			.replaceAll("=", "\\\\3D")
			.replaceAll(">", "\\\\3E")
			.replaceAll("\\(", "\\\\28")
			.replaceAll("\\)", "\\\\29")
			.replaceAll("\\*", "\\\\2A");
	}
}
